﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;

using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace ReactiveCode.Statements
{
    using Observables;
    using System.Diagnostics;
    [TestClass]
    public class StatementTests
    {
        #region Util
        List<IReactive> _methods;
        TestSyncContext _syncCtx;
        SynchronizationContext _origSyncCtx;
        ReactiveHost _host;

        [TestInitialize]
        public void InitializeTest()
        {
            _origSyncCtx = SynchronizationContext.Current;
            _syncCtx = new TestSyncContext();
            SynchronizationContext.SetSynchronizationContext(_syncCtx);

            _methods = new List<IReactive>();
            _host = new ReactiveHost();
        }
        [TestCleanup]
        public void CleanupTest()
        {
            _methods = null;
            _host.Dispose();
            _host = null;
            SynchronizationContext.SetSynchronizationContext(_origSyncCtx);
        }
        IReactive Run(params ReactiveStatement[] stmts)
        {
            var call = _host.Call(ReactiveStatement.Block(stmts));
            Wait();
            _methods.Add(call);
            return call;
        }
        void Resume()
        {
            foreach (var m in _methods)
                m.Enabled = true;
        }
        void Suspend()
        {
            foreach (var m in _methods)
            {
                m.Enabled = false;
            }
        }
        void Wait()
        {
            Reactive.SyncNow();
        }
        #endregion

        #region Factory methods
        ReactiveStatement Assign<T>(ISubject<T> target, IObservable<T> value)
        {
            return ReactiveStatement.Assignment<T>(d => target.Decorate(d), d => value.Decorate(d));
        }
        ReactiveStatement If(IObservable<bool> test, ReactiveStatement @true, ReactiveStatement @false = null)
        {
            return ReactiveStatement.If(d => test.Decorate(d), @true, @false);
        }
        #endregion

        [TestMethod]
        public void AssignmentMustKeepLhsEqualToRhs()
        {
            var rhs = new Variable<int>();
            var lhs = new Variable<int>();

            rhs.Value = 2;
            const int initial = 1;
            lhs.Value = initial;

            var assign = (Assignment<int>)Assign(lhs, rhs);
            Run(assign);
            Assert.AreEqual(rhs.Value, lhs.Value);
            Assert.AreEqual(initial, assign._latestExternal.Value);

            Suspend();
            Wait();
            Assert.AreEqual(initial, lhs.Value);

            Resume();
            Wait();
            Assert.AreEqual(rhs.Value, lhs.Value);

            rhs.Value++;
            Wait();
            Assert.AreEqual(rhs.Value, lhs.Value);

            int userValue = --lhs.Value;
            Wait();
            Assert.AreEqual(rhs.Value, lhs.Value);
            Assert.AreEqual(userValue, assign._latestExternal.Value);

            rhs.Value++;
            Wait();
            Assert.AreEqual(rhs.Value, lhs.Value);

            Suspend();
            Wait();

            Assert.AreEqual(userValue, lhs.Value);
        }

        [TestMethod]
        public void IfMustManageSubstatements()
        {
            var yes = new ReactiveStatement();
            var no = new ReactiveStatement();
            var test = new Variable<bool>();
            Run(If(test, yes, no));

            Assert.IsFalse(yes.Enabled);
            Assert.IsFalse(no.Enabled);

            test.Value = true;
            Wait();
            Assert.IsTrue(yes.Enabled);
            Assert.IsFalse(no.Enabled);

            test.Value = false;
            Wait();
            Assert.IsFalse(yes.Enabled);
            Assert.IsTrue(no.Enabled);

            Suspend();
            Wait();
            Assert.IsFalse(yes.Enabled);
            Assert.IsFalse(no.Enabled);
        }

        [TestMethod]
        public void ConditionalAssignment()
        {
            var condition = new Variable<bool>();
            var state = new Variable<int>() { Value = 0 };
            var value = Observable.Return(54);

            Run(If(condition, Assign(state, value)));

            condition.Value = true;
            Wait();
            Assert.AreEqual(54, state.Value);

            condition.Value = false;
            Wait();
            Assert.AreEqual(0, state.Value);
        }

        [TestMethod]
        public void OrderComparerTest()
        {
            /* a
             * |__b
             *    |__c
             *    |  |__d
             *    |  |__e
             *    |__f
             *       |__g
             *       |  |__h
             *       |__ i
             */

            var blocks = new List<ReactiveStatement>();
            Func<Block, Block> gen = parent =>
            {
                var block = new Block();
                if (parent != null)
                    parent.Body.Add(block);
                block.Name = ((char)('a' + blocks.Count)).ToString();
                blocks.Add(block);
                return block;
            };
            {
                var a = gen(null);
                var b = gen(a);
                var c = gen(b);
                var d = gen(c);
                var e = gen(c);
                var f = gen(b);
                var g = gen(f);
                var h = gen(g);
                var i = gen(f);
            }

            {
                for (int i = 0; i < blocks.Count; i++)
                    for (int j = 0; j < blocks.Count; j++)
                    {
                        int? cmp = ReactiveStatement.CompareOrder(blocks[i], blocks[j]);
                        Assert.IsTrue(cmp.HasValue);
                        Assert.AreEqual(Math.Sign(i - j), Math.Sign(cmp.Value));
                    }
            }
        }

        [TestMethod]
        public void StateChangesMustFollowStatementOrder()
        {
            var state = new Variable<int>();
            var value = new Variable<int>();

            List<int> executedStmts = new List<int>();
            Func<int, int> secondStmt = x =>
            {
                executedStmts.Add(1);
                return x + 1;
            };
            Func<int, int> thirdStmt = x =>
            {
                executedStmts.Add(2);
                return x * 5;
            };

            Run(ReactiveStatement.Block(
                Assign(state, value),
                ReactiveStatement.Assignment(d => state.Decorate(d), d => state.Decorate(d).Select(secondStmt)),
                ReactiveStatement.Assignment(d => state.Decorate(d), d => state.Decorate(d).Select(thirdStmt))
            ));

            for (int i = 0; i < 3; i++)
            {
                value.Value = i;
                Wait();
                ListAssert.AreEqual(executedStmts, 1, 2);
                executedStmts.Clear();
                Assert.AreEqual((i + 1) * 5, state.Value);
            }
        }

        [TestMethod]
        public void StateChangesMustFollowStatementOrder2()
        {
            var a = new Variable<long>();
            var b = new Variable<long>();
            var c = new Variable<long>();

            /*
             * a = DateTime.Now;
             * b = a;
             * a = -a;
             * c = a;
             */
            Run(ReactiveStatement.Block(
                Assign(a, Observable.Timer(TimeSpan.Zero, TimeSpan.FromMilliseconds(3))),
                Assign(b, a),
                ReactiveStatement.Assignment(d => a.Decorate(d), d => a.Decorate(d).Select(x => -x)),
                Assign(c, a)
            ));

            Stopwatch watch = Stopwatch.StartNew();
            while (watch.ElapsedMilliseconds < 10000)
            {
                Wait();
                Assert.AreEqual(-b.Value, a.Value);
                Assert.AreEqual(c.Value, a.Value);
            }
        }

    }
}
